Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PLUGIN-1849] Error Management for BigQuery Action plugin #1496

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from

Conversation

Amit-CloudSufi
Copy link

@Amit-CloudSufi Amit-CloudSufi commented Jan 17, 2025

https://cdap.atlassian.net/browse/PLUGIN-1849

[
  {
    "stageName": "BigQuery Argument Setter",
    "errorCategory": "Plugin-'BigQuery Argument Setter'",
    "errorReason": "Columns: \" env_type \"do not exist in table. Argument selections columns must exist in table.",
    "errorMessage": "Columns: \" env_type \"do not exist in table. Argument selections columns must exist in table.",
    "errorType": "USER",
    "dependency": "true"
  }
]

image

image

[
  {
    "stageName": "BigQuery Execute",
    "errorCategory": "Plugin-'Validation'-'BigQuery Execute'",
    "errorReason": "Stage 'BigQuery Execute' encountered 1 validation failures.",
    "errorMessage": "Stage 'BigQuery Execute' encountered : io.cdap.cdap.etl.api.validation.ValidationException: Errors were encountered during validation. Access Denied: Project cdf-entcon: User does not have bigquery.jobs.create permission in project cdf-entcon.. Error code: 403.",
    "errorType": "USER",
    "dependency": "false"
  }
]

Big Query Execute

image

[
  {
    "stageName": "BigQuery Execute",
    "errorCategory": "Plugin-'BigQuery Execute'",
    "errorReason": "400 Bad Request. Please check the request parameters and syntax. For more details, see https://cloud.google.com/bigquery/docs/error-messages",
    "errorMessage": "Failed to execute query with exponential backoff, io.cdap.cdap.api.exception.ProgramFailureException: com.google.api.client.googleapis.json.GoogleJsonResponseException: 400 Bad Request\nGET https://www.googleapis.com/bigquery/v2/projects/cdf-entcon/queries/f2780ae5-651c-4e52-a8ab-03eeef6f1b11?location=US&maxResults=0&prettyPrint=false\n{\n  \"code\" : 400,\n  \"errors\" : [ {\n    \"domain\" : \"global\",\n    \"location\" : \"q\",\n    \"locationType\" : \"parameter\",\n    \"message\" : \"Required field id cannot be null\",\n    \"reason\" : \"invalidQuery\"\n  } ],\n  \"message\" : \"Required field id cannot be null\",\n  \"status\" : \"INVALID_ARGUMENT\"\n}",
    "errorType": "USER",
    "dependency": "true",
    "errorCodeType": "HTTP",
    "errorCode": "400",
    "supportedDocumentationUrl": "https://cloud.google.com/bigquery/docs/error-messages"
  }
]

throw new RuntimeException(queryJob.getStatus().getError().getMessage());
String error = queryJob.getStatus().getError().getMessage();
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.USER, false, null);
Copy link
Contributor

@psainics psainics Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Job can fail on BigQuery due to many reasons like permission issues as well right? So, we need to decide based on error codes & error reason cannot directly say that it is a user error.

unknown is better.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@Amit-CloudSufi Amit-CloudSufi force-pushed the BigQueryActionPluginErrorMang branch from 7c6135e to e8b7cd6 Compare January 17, 2025 07:53
@Amit-CloudSufi Amit-CloudSufi marked this pull request as ready for review January 17, 2025 12:52
@psainics psainics added the build Trigger unit test build label Jan 17, 2025
@psainics psainics force-pushed the BigQueryActionPluginErrorMang branch from e8b7cd6 to 1b88f26 Compare January 21, 2025 10:28
@Amit-CloudSufi Amit-CloudSufi force-pushed the BigQueryActionPluginErrorMang branch from 1b88f26 to e0c7a9b Compare January 21, 2025 12:08
throw new RuntimeException(e);
String error = String.format(
"Failed to execute query with exponential backoff with message: %s", e.getMessage());
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(new RuntimeException(e), error,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to re-wrap exception here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

protected void executeQueryWithExponentialBackoff(BigQuery bigQuery,
QueryJobConfiguration queryConfig, ActionContext context)
throws Throwable {
void executeQueryWithExponentialBackoff(BigQuery bigQuery,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we changing the access modifier?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's already a final class, so we cannot have any subclasses. So I think, there is no need to make it protected anymore. Please let me know in case I missed anything.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Then we can make the access modifier private.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is still not addressed.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@psainics psainics force-pushed the BigQueryActionPluginErrorMang branch 2 times, most recently from c98fe24 to 5d4f7bc Compare January 21, 2025 15:06
}
throw e;
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, e.getCause().getMessage(),
ErrorType.SYSTEM, true, GCPUtils.BQ_SUPPORTED_DOC_URL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String errorReason = String.format("Failed to execute query with message: %s", e.getMessage())
if (cause != null) {
  errorReason = String.format("Failed to execute query with message: %s", e.getCause().getMessage())
}
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e == null ? e : e.getCause(), errorReason, ErrorType.UNKNOWN, true, GCPUtils.BQ_SUPPORTED_DOC_URL);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@@ -214,7 +239,9 @@ private void executeQuery(BigQuery bigQuery, QueryJobConfiguration queryConfig,
if (RETRY_ON_REASON.contains(queryJob.getStatus().getError().getReason())) {
throw new BigQueryJobExecutionException(queryJob.getStatus().getError().getMessage());
}
throw new RuntimeException(queryJob.getStatus().getError().getMessage());
String error = queryJob.getStatus().getError().getMessage();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String error = String.format("Failed to execute query with reason: %s, message: %s", queryJob.getStatus().getError().getReason(), queryJob.getStatus().getError().getMessage());

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

throw new RuntimeException(queryJob.getStatus().getError().getMessage());
String error = queryJob.getStatus().getError().getMessage();
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.UNKNOWN, false, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependency should be true.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

String error = String.format("The query result total rows should be \"1\" but is \"%d\"",
queryResults.getTotalRows());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.SYSTEM, true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why errorType SYSTEM

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

} catch (InterruptedException e) {
String errorMessage = String.format("The query job was interrupted: %s", e.getMessage());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessage, ErrorType.SYSTEM, true, e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ErrorType should be UNKNOWN

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

nonExistingColumnNames));
nonExistingColumnNames);
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.USER, true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependency should be false here.

Dependency is true only when error is returned from external source.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add screenshot evidences for BQExecute plugin always.

String error = String.format("The query result total rows should be \"1\" but is \"%d\"",
queryResults.getTotalRows());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.USER, true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same comment dependency will be false. dependency is true only for errors returned from external library clients.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

}
throw e;
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(
e == null ? e : (Exception) e.getCause(), errorReason, ErrorType.UNKNOWN, true, GCPUtils.BQ_SUPPORTED_DOC_URL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to cast into Exception here, instead accept Throwable here instead of Exception:

public static ProgramFailureException getHttpResponseExceptionDetailsFromChain(Exception e, String errorReason,

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@psainics psainics force-pushed the BigQueryActionPluginErrorMang branch from 19f7e9a to 1a7ee03 Compare January 22, 2025 02:47
throw new RuntimeException(queryJob.getStatus().getExecutionErrors().toString());
String error = queryJob.getStatus().getExecutionErrors().toString();
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, ErrorType.UNKNOWN, true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

create a table based on error type & error message here: https://cloud.google.com/bigquery/docs/error-messages#errortable

ErrorType will not unknown always, this comment applies to whole PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added and updated

} catch (InterruptedException e) {
String errorMessage = String.format("The query job was interrupted: %s", e.getMessage());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessage, ErrorType.UNKNOWN, false, e);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dependency should be true here

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Comment on lines 166 to 175
if (e instanceof Exception) {
String error =
String.format("Failed to execute query with exponential backoff with message: %s", e.getMessage());
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, error,
ErrorType.USER, true, GCPUtils.BQ_SUPPORTED_DOC_URL);
}
String errorMessage = String.format("Failed to execute query with exponential backoff with message: %s",
e.getMessage());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
errorMessage, errorMessage, ErrorType.UNKNOWN, true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case, we can re-wrap exception into RuntimeException and add it as cause instead of sending cause as null.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

throw new RuntimeException(e);
String error = String.format("Failed to execute query with message: %s", e.getMessage());
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(e, error, ErrorType.UNKNOWN,
true, GCPUtils.GCS_SUPPORTED_DOC_URL);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why GCS_SUPPORTED_DOC_URL for BQ errors?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

Copy link
Member

@itsankit-google itsankit-google left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add evidences of this new class being used and error getting propagated in UI correctly.

try {
queryJob.waitFor();
} catch (BigQueryException | InterruptedException e) {
String errorMessage = String.format("The query job was interrupted, %s: %s",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The bigquery query job failed, %s: %s

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

*/
public class BigQueryErrorUtil {

//https://cloud.google.com/bigquery/docs/error-messages#errortable
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add a space after //

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

errorMessage = String.format(
"Resource was not found. Please verify the resource name. If the resource will be "
+ "created at runtime, then update to use a macro for the resource name. "
+ "Error message received was %s, %s", e.getClass().getName(), e.getMessage());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error message received was %s: %s

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

String error = queryJob.getStatus().getExecutionErrors().toString();
ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
throw ErrorUtils.getProgramFailureException(new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN),
error, error, type, true, null);
Copy link
Member

@itsankit-google itsankit-google Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorReason = String.format("The bigquery job failed with reason: %s", queryJob.getStatus().getError.getReason())

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
throw ErrorUtils.getProgramFailureException(
new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason, errorReason, type,
true, null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why no supportDocUrl added?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

queryJob.getStatus().getError().getReason());
ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
throw ErrorUtils.getProgramFailureException(
new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason, errorReason, type,
Copy link
Member

@itsankit-google itsankit-google Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorMessage = queryJob.getStatus().getExecutionErrors().toString()

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

LOG.error("The query job {} failed. Error: {}", jobId.getJob(), queryJob.getStatus().getError());
LOG.error(
String.format("The query job %s failed with error %s and reason %s.", jobId.getJob(),
queryJob.getStatus().getError().getReason(), queryJob.getStatus().getError()));
Copy link
Member

@itsankit-google itsankit-google Jan 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

String.format("The query job %s failed with reason: %s and error: %s.", jobId.getJob(), queryJob.getStatus().getError().getReason(), queryJob.getStatus().getExecutionErrors().toString()))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
throw ErrorUtils.getProgramFailureException(
new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), error, error, type, true,
null);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why no supportDocUrl here?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

* @param errorReason the error reason to classify
* @return the corresponding ErrorType (USER, SYSTEM, UNKNOWN)
*/
public static String getErrorCode(String errorReason) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is this method called?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is used in BiqqueryArgumentSetter at line 119 ,

` if (queryJob.getStatus().getError() != null) {

  String errorReason = String.format(
      "The bigquery job failed with reason: %s. For more details, see %s",
      queryJob.getStatus().getError().getReason(), GCPUtils.BQ_SUPPORTED_DOC_URL);
  ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
  String errorCode = BigQueryErrorUtil.getErrorCode(
      queryJob.getStatus().getError().getReason());
  throw ErrorUtils.getProgramFailureException(
      new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason,
      queryJob.getStatus().getExecutionErrors().toString(), type, true, ErrorCodeType.HTTP,
      errorCode, GCPUtils.BQ_SUPPORTED_DOC_URL, null);
}`

Comment on lines 69 to 97
ERROR_REASON_TO_ERROR_CODE.put("invalid", 400);
ERROR_REASON_TO_ERROR_CODE.put("invalidQuery", 400);
ERROR_REASON_TO_ERROR_CODE.put("billingTierLimitExceeded", 400);
ERROR_REASON_TO_ERROR_CODE.put("resourceInUse", 400);
ERROR_REASON_TO_ERROR_CODE.put("resourcesExceeded", 400);
ERROR_REASON_TO_ERROR_CODE.put("badRequest", 400);
ERROR_REASON_TO_ERROR_CODE.put("invalidUser", 400);
ERROR_REASON_TO_ERROR_CODE.put("notFound", 404);
ERROR_REASON_TO_ERROR_CODE.put("duplicate", 409);
ERROR_REASON_TO_ERROR_CODE.put("accessDenied", 403);
ERROR_REASON_TO_ERROR_CODE.put("billingNotEnabled", 403);
ERROR_REASON_TO_ERROR_CODE.put("quotaExceeded", 403);
ERROR_REASON_TO_ERROR_CODE.put("rateLimitExceeded", 403);
ERROR_REASON_TO_ERROR_CODE.put("responseTooLarge", 403);
ERROR_REASON_TO_ERROR_CODE.put("blocked", 403);
ERROR_REASON_TO_ERROR_CODE.put("proxyAuthenticationRequired", 407);

// System Errors
ERROR_REASON_TO_ERROR_CODE.put("tableUnavailable", 400);
ERROR_REASON_TO_ERROR_CODE.put("backendError", 503);
ERROR_REASON_TO_ERROR_CODE.put("internalError", 500);
ERROR_REASON_TO_ERROR_CODE.put("notImplemented", 501);
ERROR_REASON_TO_ERROR_CODE.put("jobBackendError", 400);
ERROR_REASON_TO_ERROR_CODE.put("jobInternalError", 400);
ERROR_REASON_TO_ERROR_CODE.put("jobRateLimitExceeded", 400);
ERROR_REASON_TO_ERROR_CODE.put("timeout", 400);

// Unknown Errors
ERROR_REASON_TO_ERROR_CODE.put("stopped", 200);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think ErrorCode is fixed per ErrorReason:
image

Let's not assume this instead just use ErrorReason.

Copy link
Author

@Amit-CloudSufi Amit-CloudSufi Jan 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so , should we pass null in error code, instead of checking here

` if (queryJob.getStatus().getError() != null) {

  String errorReason = String.format(
      "The bigquery job failed with reason: %s. For more details, see %s",
      queryJob.getStatus().getError().getReason(), GCPUtils.BQ_SUPPORTED_DOC_URL);
  ErrorType type = BigQueryErrorUtil.getErrorType(queryJob.getStatus().getError().getReason());
  String errorCode = BigQueryErrorUtil.getErrorCode(
      queryJob.getStatus().getError().getReason());
  throw ErrorUtils.getProgramFailureException(
      new ErrorCategory(ErrorCategory.ErrorCategoryEnum.PLUGIN), errorReason,
      queryJob.getStatus().getExecutionErrors().toString(), type, true, ErrorCodeType.HTTP,
      errorCode, GCPUtils.BQ_SUPPORTED_DOC_URL, null);
}`

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, you can pass both errorCode & errorCodeType as null

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done, updated. and removed the error codes form util class also

}
throw e;
throw GCPErrorDetailsProviderUtil.getHttpResponseExceptionDetailsFromChain(
e == null ? e : e.getCause(), errorReason, ErrorType.UNKNOWN, true,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

e.getCause() == null

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated

@Amit-CloudSufi Amit-CloudSufi force-pushed the BigQueryActionPluginErrorMang branch from 4bc048c to 5821daa Compare January 30, 2025 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
build Trigger unit test build
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants